package com.vast.admin.api;

import com.google.gson.Gson;
import com.google.gson.reflect.TypeToken;
import com.vast.admin.api.common.HttpClientSender;
import com.vast.admin.api.common.RSAUtils;
import com.vast.admin.api.entity.*;
import com.vast.admin.api.response.*;
import com.vast.common.constant.MessageCode;
import com.vast.common.core.domain.Result;
import com.vast.system.exception.BusinessException;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;

import java.io.IOException;
import java.lang.reflect.Type;
import java.util.Map;

/**
 * 上上签混合云SDK客户端
 */
@Component
@Slf4j
public class BestsignOpenApi {

    @Value("${ssqApi.url}")
    private String serverHost = "https://openapi.bestsign.info/openapi/v2";

    @Value("${ssqApi.developerId}")
    private String developerId = "1575361718011460686";

    @Value("${ssqApi.publicKey}")
    private String publicKey = "MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDbrv7Q02pGgsrlbhqIiYE31ka7k5gcxbvT5kF7T2EcrZ48BzZh13u4+3G1Pc8v47FIo2acT59kMoe4TuP5NVFnUhFdqhWVkCy9ueZRnNKYv/b7/z9g6v4eBSlU+9AI3A2P8JeGo0sDsiSz3YsONfcZQovc0tk0nWkdtqkbhNHcyQIDAQAB";

    @Value("${ssqApi.privateKey}")
    private String privateKey = "MIICdgIBADANBgkqhkiG9w0BAQEFAASCAmAwggJcAgEAAoGBANuu/tDTakaCyuVuGoiJgTfWRruTmBzFu9PmQXtPYRytnjwHNmHXe7j7cbU9zy/jsUijZpxPn2Qyh7hO4/k1UWdSEV2qFZWQLL255lGc0pi/9vv/P2Dq/h4FKVT70AjcDY/wl4ajSwOyJLPdiw419xlCi9zS2TSdaR22qRuE0dzJAgMBAAECgYAlApFZWxS/dk96YZdEw22qm4XlGhN5XXKsW1FS1ee1biaHVGu8BhKI8ouQji42kjSO2UBzOxLOTZkB2wCbPnlND8i6pLoIeJRbTzM4Tc7DZJKb/Y3sfRRmc7pYroNm24Ugij1/K1t8zgwCgo0dqIebj3esj/F1+U7kBSU0AAy7FQJBAPoWDD8TcCOtjUIMBhib4xh+dwnBqdU5QAIq7AmV4bZevHJQGJ9etZilglMm9VZteNQVqZc5SBd7NthRActV71cCQQDg4OYf2IudZpdbKLMK7LG6WClSp9f8f57fVoInKNjrHVqXY5GEVjdjR6wu4iIQLSJphIFhL5yDODrBeNYPoKDfAkB0FM/SZ19h8CjlQ1eBDLq4n9r5OF9wjYZcezfcqyl90TN/jBYhw4BfPlPj7O1sFJxFNVjmzLvdyl+GwPMoQtc5AkAdOJwunzDlLyOT0OITFVay5Cnad7z1LpAybUdUV1Z4lsPi0ClkjN99ox6YqQHMIFFMbM9zFYC8/F2GLnGCblj5AkEA+YhPKdQ1JpFRhdnaH+FzU1MIl0Wq7f+dyNK8s/c//jNDWQ8rfAkiVwLcS5pEt+lYk/bhOD97e63KM0FFAtr38w==";

    public static String account = "18186148796";

    private static String urlSignParams = "?developerId=%s&rtick=%s&signType=rsa&sign=%s";

    private Gson gson = new Gson();

    public String getAccount() {
        return account;
    }

    @Value("${ssqApi.account}")
    public void setAccount(String account) {
        BestsignOpenApi.account = account;
    }

    /**
	 * 企业注册并申请证书
	 * @return 异步申请任务单号
	 * @throws IOException
	 */
	public BestsignResponse<AsynTaskResponse> companyReg(RegUser regUser) {
        String method = "/user/reg/";
		return doPost(method, null, regUser, new TypeToken<BestsignResponse<AsynTaskResponse>>(){}.getType());
	}

    /**
     * 个人注册不申请证书
     * @param regUser
     * @return
     */
	public BestsignResponse<Void> personalReg(RegUser regUser) {
        String method = "/user/reg/";
        return doPost(method, null, regUser, new TypeToken<BestsignResponse<Void>>(){}.getType());
    }

    /**
     * 设置个人信息
     * @param setPersonalCredential
     * @return
     */
    public BestsignResponse<Void> setPersonalCredential(SetPersonalCredential setPersonalCredential) {
        String method = "/user/setPersonalCredential/";
        return doPost(method, null, setPersonalCredential, new TypeToken<BestsignResponse<Void>>(){}.getType());
    }

    /**
     * 申请专用数字证书 (申请个人证书使用)
     * 申请证书所使用的证件信息请先核实确认正确无误后再调用本接口。
     * ①　为用户唯一标识申请专用的数字证书，该数字证书用于电子签名，并且在上上签的体系内是该用户独有的，不会被其他开发者用相同证件信息申请证书的行为复用到该证书。
     * ②　申请专用数字证书前必须先调用“注册用户”，然后设置用户证件信息（调用“设置个人用户证件信息”或“设置企业用户证件信息”接口）。
     * @param bestsignAccount
     * @return
     */
    public BestsignResponse<ApplyCertResponse> applyCert(BestsignAccount bestsignAccount) {
        String method = "/user/applyCert/unique/";
        return doPost(method, null, bestsignAccount, new TypeToken<BestsignResponse<ApplyCertResponse>>(){}.getType());
    }

    /**
     * 查询用户注册证书申请状态
     * @param applyCertStatus
     * @return
     */
	public BestsignResponse<AsynTaskResponse> applyCertStatus(ApplyCertStatus applyCertStatus) {
        String method = "/user/async/applyCert/status/";
        return doPost(method, null, applyCertStatus, new TypeToken<BestsignResponse<AsynTaskResponse>>(){}.getType());
    }

    /**
     * 核验上上签SaaS用户实名认证状态
     * @param realNameStatus
     * @return
     */
    public BestsignResponse<RealNameStatusResponse> realNameStatus(RealNameStatus realNameStatus) {
	    String method = "/realName/status/bestsign/";
        return doPost(method, null, realNameStatus, new TypeToken<BestsignResponse<RealNameStatusResponse>>(){}.getType());
    }

    /**
     * 生成企业印章
     * @param createSignature
     * @return
     */
    public BestsignResponse<Void> createSignatureImage(CreateSignature createSignature) {
        String method = "/dist/signatureImage/ent/create/";
        return doPost(method, null, createSignature, new TypeToken<BestsignResponse<Void>>(){}.getType());
    }

    /**
     * 下载用户签名/印章图片
     * @param account 用户账号 下载哪个用户的签名/印章图片就填该用户的帐号
     * @param imageName 签名图片名称 空字符串和default都表示默认的签名/印章图片。 企业用户如需下载自定义印章则此处填写上传时指定的印章名称
     * @return
     */
    public byte[] downloadSignatureImage(String account, String imageName) {
        String method = "/signatureImage/user/download/";
        // 组装url参数
        String urlParams = "account=" + account + "&imageName=" + imageName;
        return doGet(method, urlParams);
    }

    /**
     * 下载合同签约存证页文件
     * 用此接口可以下载签约存证页文件，如未生成过签约存证页文件则此接口会先生成签约存证页文件再下载。
     * @return
     */
    public byte[] downloadAttachment(String contractId) {
        String method = "/contract/downloadAttachment/";
        // 组装url参数
        String urlParams = "contractId=" + contractId + "&isShowFacePhoto=1";
        return doGet(method, urlParams);
    }

    /**
     * 下载目录签约存证页文件(多文档)
     * @param catalogName
     * @return
     */
    public byte[] downloadCatalogAttachment(String catalogName) {
        String method = "/catalog/downloadAttachment/";
        // 组装url参数
        String urlParams = "catalogName=" + catalogName + "&isShowFacePhoto=1";
        return doGet(method, urlParams);
    }


	/**
	 * 下载合同PDF文件
	 *
	 * @param contractId 合同编号
	 * @return
	 * @throws Exception
	 */
	public byte[] contractDownload(String contractId) {
		String method = "/storage/contract/download/";
		// 组装url参数
		String urlParams = "contractId=" + contractId;
		return doGet(method, urlParams);
	}

    /**
     * 银行卡四要素认证及发送验证码
     * 此接口用于个人用户姓名、身份证号、银行卡号、银行卡预留手机号4要素校验, 校验通过后向该手机号发送短信验证码。
     * 该接口需与接口“银行卡多要素验证码校验”联合使用。获取到的验证码有效期为3分钟，用于校验的personalIdentity4Key为永久有效，
     * 但重新获取验证码后需要使用新personalIdentity4Key。此为单独计费接口，请确认已购买并开通此接口权限。
     * https://openapi.bestsign.info/#/dev/doc/03022401?defineType=page
     * @return
     */
	public BestsignResponse<RealNameByIdentity4Response> realNameByIdentity4(RealNameByIdentity4 realNameByIdentity4) {
        String method = "/realName/personal/identity4/vcode/sender/";
        return doPost(method, null, realNameByIdentity4, new TypeToken<BestsignResponse<RealNameByIdentity4Response>>(){}.getType());
    }

    /**
     * 银行卡四要素验证码校验
     * 此接口用于对接口“银行卡多要素校验及发送验证码”中的手机验证码进行校验。
     * @return
     */
    public BestsignResponse<RealNameIdentity4VerifyResponse> realNameIdentity4Verify(RealNameIdentity4Verify realNameIdentity4Verify) {
        String method = "/realName/personal/identity4/vcode/verify/";
        return doPost(method, null, realNameIdentity4Verify, new TypeToken<BestsignResponse<RealNameIdentity4VerifyResponse>>(){}.getType());
    }

    /**
     * 个人银行卡多要素验证
     * 验证个人“姓名、身份证号、银行卡号、银行卡预留手机号码”多要素之间的一致性。此接口按照调用次数单独计费。
     * @return
     */
    public BestsignResponse<RealNameIdentity4VerifyResponse> realNameIdentity4VerifyWithoutSmsCode(RealNameByIdentity4 realNameByIdentity4) {
        String method = "/credentialVerify/personal/identity4/";
        return doPost(method, null, realNameByIdentity4, new TypeToken<BestsignResponse<RealNameIdentity4VerifyResponse>>(){}.getType());
    }

    /**
     * 个人手机号三要素验证码获取
     * 此接口用于个人用户姓名、身份证、手机号3要素收集校验, 以及验证码的发送。
     * 该接口需与“个人手机号三要素验证码校验”接口搭配使用。获取到的验证码有效期为3分钟，用于校验的personalIdentity3Key为永久有效，
     * 但重新获取验证码后需要使用新personalIdentity3Key。
     * 本接口单独计费，请确认已购买并开通此接口权限。
     * https://openapi.bestsign.info/#/dev/doc/03021103?defineType=page
     * @param realNameByIdentity3
     * @return
     */
    public BestsignResponse<RealNameByIdentity3Response> realNameByIdentity3(RealNameByIdentity3 realNameByIdentity3) {
        String method = "/realName/personal/identity3/vcode/sender/";
        return doPost(method, null, realNameByIdentity3, new TypeToken<BestsignResponse<RealNameByIdentity3Response>>(){}.getType());
    }

    /**
     * 个人手机号三要素验证码校验
     * 此接口用于对接口“个人手机号三要素验证码获取”中的手机验证码验证和多要素（个人姓名、身份证号、手机号码）之间的一致性校验。
     * @param realNameIdentity3Verify
     * @return
     */
    public BestsignResponse<RealNameIdentity3VerifyResponse> realNameIdentity3Verify(RealNameIdentity3Verify realNameIdentity3Verify) {
        String method = "/realName/personal/identity3/vcode/verify/";
        return doPost(method, null, realNameIdentity3Verify, new TypeToken<BestsignResponse<RealNameIdentity3VerifyResponse>>(){}.getType());
    }

    /**
     * 个人手机号三要素验证
     * 验证个人“姓名、身份证号、手机号码”三要素之间的一致性。手机号码为三大运营商手机号码。此接口按照调用次数单独计费。
     * @param realNameByIdentity3
     * @return
     */
    public BestsignResponse<RealNameIdentity3VerifyResponse> realNameIdentity3VerifyWithoutSmsCode(RealNameByIdentity3 realNameByIdentity3) {
        String method = "/credentialVerify/personal/identity3/mobile/";
        return doPost(method, null, realNameByIdentity3, new TypeToken<BestsignResponse<RealNameIdentity3VerifyResponse>>(){}.getType());
    }

    /**
     * 实名认证 刷脸认证
     * 利用个人“接口账号、姓名、身份证号等”要素生成唯一的认证url地址，通过该url地址跳转到腾讯云的H5认证页面。此接口单独计费。
     *
     * 说明：用户通过上述url地址进行完刷脸及可信源人脸比对，可以得到认证结果，上上签在跳转到returnUrl的同时，
     *      会通过POST提交一个Form表单到该returnUrl告知认证结果。
     *      报文示例如下：
     * POST / HTTP/1.1
     * Host: 192.168.30.190:7777
     * Content-Length: 50
     * Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,/;q=0.8
     * Accept-Encoding: gzip, deflate
     * Accept-Language: zh-CN,zh;q=0.9,en-US;q=0.8,en;q=0.7
     * Cache-Control: max-age=0
     * Content-Type: application/x-www-form-urlencoded
     *
     * data=Basdkjadasiadjrihii2178eqe….du89q2ue3%3D%3D
     *
     * 其中data为JSON格式字符串base64编码，解析之后的格式如下：
     *
     * {
     * “bizNo”: “123456789098765”,
     * “similarity”: “88.0”,
     * “verifyResult”: “1”,
     * “photo”: “……”,
     * “sid”: “13123123123213”
     * }
     *
     * 参数名	说明
     * bizNo	流水号
     * similarity	相似度，字符串格式
     * verifyResult	1：认证通过 0 ：认证不通过
     * photo	图片base64编码（一张）
     * sid	传入的认证流水号
     * @param realNameByFace
     * @return
     */
    public BestsignResponse<RealNameByFaceResponse> realNameByFace(RealNameByFace realNameByFace) {
        String method = "/realName/personal/identity2/face";
        return doPost(method, null, realNameByFace, new TypeToken<BestsignResponse<RealNameByFaceResponse>>(){}.getType());
    }

    /**
     * 上传并创建合同
     * 说明：
     *       此接口是将“上传合同文件/storage/upload”与“创建合同/contract/create”的封装在一起，方便开发者调用。
     *          调用此接口后，将文件保存到自定义的存储中，暂时只支持上传PDF文件。上传成功后得到一个合同编号，后续流程可以直接使用此合同编号进行签署。
     *       如果开发者需要用到空白合同当模版，添加不同的PDF元素时，请分开使用“上传合同文件/storage/upload”、
     *          “PDF文件添加元素/storage/addPDFElements”与“创建合同/contract/create”。
     * @param uploadContract
     * @return
     */
    public BestsignResponse<UploadContractResponse> uploadContract(UploadContract uploadContract) {
        String method = "/storage/contract/upload/";
        return doPost(method, null, uploadContract, new TypeToken<BestsignResponse<UploadContractResponse>>(){}.getType());
    }

    /**
     * 签署合同（即自动签）
     * 说明：签署合同，可直接用于无感知的快捷签署。此接口要求签署者必须拥有自己的数字证书。可以无需先调用“添加签署者”接口与“设置合同签署参数”接口。
     * @param signContract
     * @return
     */
    public BestsignResponse<Void> signContract(SignContract signContract) {
        String method = "/contract/sign/cert/";
        return doPost(method, null, signContract, new TypeToken<BestsignResponse<Void>>(){}.getType());
    }

    /**
     * 单个添加签署者
     * 为已创建的合同添加签署者，添加签署者之后的合同正式发送。
     * @param addSigner
     * @return
     */
    public BestsignResponse<Void> addSigner(AddSigner addSigner) {
        String method = "/contract/addSigner/";
        return doPost(method, null, addSigner);
    }


    /**
     * 获取手动签URL
     * 说明：获取合同手动签署的页面URL，可以根据设置的合同签署参数选择进行短信校验、手绘签名等操作，并在此页面完成签署。
     * @param getSignURL
     * @return
     */
    public BestsignResponse<SendContractResponse> getSignURL(GetSignURL getSignURL) {
        String method = "/contract/getSignURL/";
        return doPost(method, null, getSignURL, new TypeToken<BestsignResponse<SendContractResponse>>(){}.getType());
    }

    /**
     * 锁定并结束合同
     * 说明：
     *      当所有签署者都签署完毕，确认合同签署流程可以完结后，使用此接口添加上上签系统的锁定签名，并且结束整个合同签署过程，将合同状态更新为“已完成”，不允许任何人再进行签署。
     *      只有已结束的合同才能提供公证服务。
     * @param lockContract
     * @return
     */
    public BestsignResponse<Void> lockContract(LockContract lockContract) {
        String method = "/storage/contract/lock/";
        return doPost(method, null, lockContract, new TypeToken<BestsignResponse<Void>>(){}.getType());
    }

    /**
     * 创建合同目录
     * 使用多文档签署功能，必须先创建一个合同目录，用来容纳需要同时签署的多份合同文件。
     * 同一个开发者创建的合同目录不能重名。
     * @param createCatalog
     * @return
     */
    public BestsignResponse<Void> createCatalog(CreateCatalog createCatalog) {
        String method = "/catalog/create/";
        return doPost(method, null, createCatalog, new TypeToken<BestsignResponse<Void>>(){}.getType());
    }

    /**
     * 向目录添加合同文件（根据fid或者fdata添加）
     * 说明：
     *      创建好合同目录后，将需要同时签署的合同文件添加到目录中。
     *      要一份份单独的添加，支持采用“文件编号”和“PDF文件”2种方式，
     *      2种方式二选一即可：
     *          > 通过“文件编号”添加——实现上传好合同文件，然后在此接口使用文件编号进行添加，用到的参数为“fid”；
     *          > 通过“PDF文件”添加——在此接口提交合同PDF文件（base64编码），用到的参数为“fmd5，fname，ftype，fpages，fdata”；
     * @param uploadCatalogContract
     * @return
     */
    public BestsignResponse<UploadCatalogContractResponse> uploadCatalogContract(UploadCatalogContract uploadCatalogContract) {
        String method = "/catalog/uploadContract/";
        return doPost(method, null, uploadCatalogContract, new TypeToken<BestsignResponse<UploadCatalogContractResponse>>(){}.getType());
    }

    /**
     * 获取多文档手动签署URL
     * 获取合同手动签署的页面URL，将此URL发送给指定用户，即可在此页面进行手动签操作完成签署。
     * 在此页面上实现“同时查看多份合同文件、只校验一次验证码，同时签署多份合同文件”的目的。
     * 查看合同时图片按合同编号排序。
     * 请注意：本接口是签署接口，只能查看到需要签署的那几份合同，如果目录中有些合同不需要签署则不展示这些合同。
     * @param sendContract
     * @return
     */
    public BestsignResponse<SendContractResponse> sendCatalog(SendContract sendContract) {
        String method = "/catalog/send/";
        return doPost(method, null, sendContract, new TypeToken<BestsignResponse<SendContractResponse>>(){}.getType());
    }

    /**
     * 锁定并结束目录
     * 当一个合同目录下的所有签署者都签署完毕后，使用此接口添加上上签系统的锁定签名，并且结束整个合同签署过程，将合同状态更新为“已完成”，不允许任何人再进行签署。
     * 只有结束的合同才能提供公证服务。如果开发者需要下载签署完成的合同PDF文件给其他电子签约系统（比如资金方用的友商的电子签约系统）签署，那么请使用“8.13.10、结束目录”接口结束目录。
     * @param lockCatalog
     * @return
     */
    public BestsignResponse<Map<String, LockCatalogResponse>> lockCatalog(LockCatalog lockCatalog) {
        String method = "/catalog/lock/";
        return doPost(method, null, lockCatalog);
    }



    /**
     * 设置合同签署参数
     * 为签署者设置合同签署参数，包括签署位置的坐标（如坐标为设置则以签署接口为准）、使用的签名/印章图片等。
     * @param setSignerConfig
     * @return
     */
    public BestsignResponse<Void> setSignerConfig(SetSignerConfig setSignerConfig) {
        String method = "/contract/setSignerConfig/";
        return doPost(method, null, setSignerConfig);
    }

    /**
     * 查询合同签署者状态
     * data内容为合同签署者状态，为json格式，格式为“account - 状态码”成对出现。状态码：1 － 未签署; 2 - 已签署。
     * @param contractId
     * @return
     */
    public BestsignResponse<Map<String, String>> getSignerStatus(ContractId contractId) {
        String method = "/contract/getSignerStatus/";
        return doPost(method, null, contractId, new TypeToken<BestsignResponse<Map<String, String>>>(){}.getType());
    }

    /**
     * 获取合同列表
     * 添加完合同文件后，使用此接口获取合同文件的列表，返回值中的“contractId”用于后续设置每个签署者的签署坐标等参数。
     * @param catalogName
     * @return
     */
    public BestsignResponse<GetContractsResponse> getContracts(CatalogName catalogName) {
        String method = "/catalog/getContracts/";
        return doPost(method, null, catalogName, new TypeToken<BestsignResponse<GetContractsResponse>>(){}.getType());
    }

    /**
     * 单文档合同预览链接
     * @return
     */
    public BestsignResponse<PreviewURLResponse> getPreviewURL(PreviewURL previewURL) {
        String method = "/contract/getPreviewURL/";
        return doPost(method, null, previewURL, new TypeToken<BestsignResponse<PreviewURLResponse>>(){}.getType());
    }

    /**
     * 多文档合同预览链接
     * @return
     */
    public BestsignResponse<PreviewURLResponse> getCatalogPreviewURL(PreviewURL previewURL) {
        String method = "/catalog/getPreviewURL/";
        return doPost(method, null, previewURL, new TypeToken<BestsignResponse<PreviewURLResponse>>(){}.getType());
    }

    /**
     * 获取刷脸结果
     * @return
     */
    public BestsignResponse<GetFaceAuthResultResponse> getFaceAuthResult(GetFaceAuthResult getFaceAuthResult) {
        String method = "/realName/personal/identity2/getFaceAuthResult/";
        return doPost(method, null, getFaceAuthResult, new TypeToken<BestsignResponse<GetFaceAuthResultResponse>>(){}.getType());
    }

    /**
     * 撤销合同
     * 说明：当发现合同内容出错等情况时，可用来直接取消未结束的合同（由开发者发起的取消，不需要确认签署者），避免造成错误的合同完成了签署。
     * @param contractId
     * @return
     */
    public BestsignResponse<Void> cancel(ContractId contractId) {
        String method = "/contract/cancel/";
        return doPost(method, null, contractId, new TypeToken<BestsignResponse<Void>>(){}.getType());
    }

    /**
     * 获取H5刷脸签署的订单号
     * 因签署接口没有直接返回刷脸的orderNo，故通过此接口获取刷脸签署通过后产生的orderNo，通过此orderNo可以下载刷脸签署时的视频或照片文件。
     * @param contractId
     * @return
     */
    public BestsignResponse<OrderNo> getFaceAuthOrderNo(ContractId contractId) {
        String method = "/contract/getFaceAuthOrderNo";
        return doPost(method, null, contractId, new TypeToken<BestsignResponse<OrderNo>>(){}.getType());
    }

    /**
     * 获取刷脸认证/签署的视频或照片
     * 获取刷脸校验通过后的视频或照片。
     * 时效性：目前腾讯云对刷脸视频或照片文件最多保存一年，阿里云对刷脸照片文件最多保存一周，故如需获取请在刷脸后的一周内（腾讯云可在一年内）获取。
     * 支持范围：仅支持腾讯云H5刷脸方式；支付宝刷脸方式因支付宝的安全策略限制照片无法获取。
     * @param orderNo
     * @return
     */
    public BestsignResponse<CredentialVerifyVideo> downloadCredentialVerifyVideo(OrderNo orderNo) {
        String method = "/credentialVerify/face/auth/result/download";
        return doPost(method, null, orderNo, new TypeToken<BestsignResponse<CredentialVerifyVideo>>(){}.getType());
    }

    /**
     * post请求
     * @param method
     * @param urlParams
     * @param param
     * @param <T>
     * @return
     */
    private <T> BestsignResponse<T> doPost(String method, String urlParams, Object param) {
        String jsonParam = null;
        if (param != null) {
            jsonParam = gson.toJson(param);
        }
        // 生成一个时间戳参数
        String rtick = RSAUtils.getRtick();
        // 计算参数签名
        String paramsSign = RSAUtils.calcRsaSign(developerId, privateKey, serverHost, method, rtick, urlParams, jsonParam);
        // 签名参数追加为url参数
        if (urlParams != null) {
            urlParams = String.format(urlSignParams, developerId, rtick, paramsSign) + "&" + urlParams;
        } else {
            urlParams = String.format(urlSignParams, developerId, rtick, paramsSign);
        }
        // 发送请求
        String responseBody = null;
        try {
            responseBody = HttpClientSender.sendHttpPost(serverHost, method, urlParams, jsonParam);
        } catch (IOException e) {
            e.printStackTrace();
        }
        if (responseBody != null) {
            Type type = new TypeToken<BestsignResponse<Void>>(){}.getType();
            return gson.fromJson(responseBody, type);
        }
        return null;
    }

    /**
     * post请求
     * @param method
     * @param urlParams
     * @param param
     * @param <T>
     * @return
     */
    private <T> BestsignResponse<T> doPost(String method, String urlParams, Object param, Type type) {
        String jsonParam = null;
        if (param != null) {
            jsonParam = gson.toJson(param);
        }
        // 生成一个时间戳参数
        String rtick = RSAUtils.getRtick();
        // 计算参数签名
        String paramsSign = RSAUtils.calcRsaSign(developerId, privateKey, serverHost, method, rtick, urlParams, jsonParam);
        // 签名参数追加为url参数
        if (urlParams != null) {
            urlParams = String.format(urlSignParams, developerId, rtick, paramsSign) + "&" + urlParams;
        } else {
            urlParams = String.format(urlSignParams, developerId, rtick, paramsSign);
        }
        // 发送请求
        String responseBody = null;
        try {
            responseBody = HttpClientSender.sendHttpPost(serverHost, method, urlParams, jsonParam);
            if (method.indexOf("upload") > -1) {
                //上传时的参数包含base64的合同PDF文件，不在日志输出参数
                log.info("method: {}, result: {}", method, responseBody);
            } else {
                log.info("method: {}, param: {}, result: {}", method, jsonParam, responseBody);
            }
        } catch (IOException e) {
            log.error(e.getMessage(), e);
        }
        if (responseBody != null) {
            BestsignResponse bestsignResponse = gson.fromJson(responseBody, type);
            if (bestsignResponse.getErrno().intValue() != 0) {
                throw new BusinessException(new Result(MessageCode.ERROR, bestsignResponse.getErrmsg()));
            }
            return bestsignResponse;
        }
        return null;
    }

    /**
     * get 请求
     * @param method
     * @param urlParams
     * @return
     */
    private byte[] doGet(String method, String urlParams) {
        // 生成一个时间戳参数
        String rtick = RSAUtils.getRtick();
        // 计算参数签名
        String paramsSign = RSAUtils.calcRsaSign(developerId, privateKey, serverHost, method, rtick, urlParams, null);
        // 签名参数追加为url参数
        urlParams = String.format(urlSignParams, developerId, rtick, paramsSign) + "&" + urlParams;
        // 发送请求
        byte[] responseBody = new byte[0];
        try {
            responseBody = HttpClientSender.sendHttpGet(serverHost, method, urlParams);
        } catch (IOException e) {
            e.printStackTrace();
        }
        // 返回结果解析
        return responseBody;
    }

    public String getDownloadUrl(String contractId) {
        String method = "/storage/contract/download/";
        // 组装url参数
        String urlParams = "contractId=" + contractId;

        // 生成一个时间戳参数
        String rtick = RSAUtils.getRtick();
        // 计算参数签名
        String paramsSign = RSAUtils.calcRsaSign(developerId, privateKey, serverHost, method, rtick, urlParams, null);
        // 签名参数追加为url参数
        urlParams = String.format(urlSignParams, developerId, rtick, paramsSign) + "&" + urlParams;

        return serverHost + method + urlParams;
    }
}
